В связи с решением открыть небольшое, но оригинальное кафе в Москве, в котором гостей будут обслуживать роботы, мы подготовили исследование текущей ситуации на рынке общественного питания для потенциальных инвесторов, в результате которого постараемся ответить на вопрос: «получится ли у нашего кафе снискать популярность на долгое время, когда все зеваки насмотрятся на роботов-официантов?». В исследовании будем руководствоваться открытыми данными о заведениях общественного питания в Москве.
# Импортируем необходимые библиотеки
import pandas as pd
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from matplotlib import pyplot as plt
#import numpy as np
# Чтобы не беспокоили ворнинги
import warnings
warnings.simplefilter("ignore")
df = pd.read_csv('/datasets/rest_data.csv') # сохранили датасет в переменную df
df.head(30) # убедились, что сохранение прошло успешно, также познакомились с первыми 30-ю строками
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 0 | 151635 | СМЕТАНА | нет | кафе | город Москва, улица Егора Абакумова, дом 9 | 48 |
| 1 | 77874 | Родник | нет | кафе | город Москва, улица Талалихина, дом 2/1, корпус 1 | 35 |
| 2 | 24309 | Кафе «Академия» | нет | кафе | город Москва, Абельмановская улица, дом 6 | 95 |
| 3 | 21894 | ПИЦЦЕТОРИЯ | да | кафе | город Москва, Абрамцевская улица, дом 1 | 40 |
| 4 | 119365 | Кафе «Вишневая метель» | нет | кафе | город Москва, Абрамцевская улица, дом 9, корпус 1 | 50 |
| 5 | 27429 | СТОЛ. ПРИ ГОУ СОШ № 1051 | нет | столовая | город Москва, Абрамцевская улица, дом 15, корп... | 240 |
| 6 | 148815 | Брусника | да | кафе | город Москва, переулок Сивцев Вражек, дом 6/2 | 10 |
| 7 | 20957 | Буфет МТУСИ | нет | столовая | город Москва, Авиамоторная улица, дом 8, строе... | 90 |
| 8 | 20958 | КПФ СЕМЬЯ-1 | нет | столовая | город Москва, Авиамоторная улица, дом 8, строе... | 150 |
| 9 | 28858 | Столовая МТУСИ | нет | столовая | город Москва, Авиамоторная улица, дом 8, строе... | 120 |
| 10 | 148595 | Пекарня 24 | нет | закусочная | город Москва, Авиамоторная улица, дом 47 | 5 |
| 11 | 23394 | Гогиели | нет | кафе | город Москва, Авиамоторная улица, дом 49/1 | 24 |
| 12 | 28582 | ШКОЛА 735 | нет | столовая | город Москва, Авиамоторная улица, дом 51 | 140 |
| 13 | 22579 | Алло Пицца | да | кафе | город Москва, улица Авиаторов, дом 14 | 32 |
| 14 | 23670 | Гимназия 1542 | нет | столовая | город Москва, улица Авиаторов, дом 16 | 270 |
| 15 | 23663 | Школа 1011 | нет | столовая | город Москва, улица Авиаторов, дом 18 | 320 |
| 16 | 144107 | Суши Wok | да | предприятие быстрого обслуживания | город Москва, Азовская улица, дом 3 | 7 |
| 17 | 154654 | Донер Кебаб | нет | предприятие быстрого обслуживания | город Москва, Азовская улица, дом 4 | 2 |
| 18 | 58565 | Тануки | да | ресторан | город Москва, Большая Академическая улица, дом 65 | 160 |
| 19 | 153644 | Американская Лаборатория Десертов | нет | кафе | город Москва, Филипповский переулок, дом 15/5 | 20 |
| 20 | 21950 | Кафе | нет | кафе | город Москва, Алтайская улица, дом 33/7 | 30 |
| 21 | 84832 | КАФЕ УЮТ | нет | кафе | город Москва, Алтуфьевское шоссе, дом 14 | 110 |
| 22 | 26931 | Долина Чайхона | нет | кафе | город Москва, Алтуфьевское шоссе, дом 14 | 150 |
| 23 | 28751 | ГБОУ Школа № 1411 (970) | нет | столовая | город Москва, Алтуфьевское шоссе, дом 42Б | 120 |
| 24 | 125608 | Кафетерий | нет | кафетерий | город Москва, Алтуфьевское шоссе, дом 56 | 6 |
| 25 | 81554 | Домино'с Пицца | да | кафе | город Москва, Алтуфьевское шоссе, дом 56 | 24 |
| 26 | 21304 | РАХИМКУЛОВА Т.Х. | нет | буфет | город Москва, Алтуфьевское шоссе, дом 102Б | 20 |
| 27 | 29223 | СТОЛОВАЯ ПРИ ГУП ОБЪЕДИНЕННЫЙ КОМБИНАТ ШКОЛЬНО... | нет | столовая | город Москва, улица Амундсена, дом 10 | 192 |
| 28 | 27439 | ШКОЛА 1444 | нет | столовая | город Москва, Анадырский проезд, дом 55 | 240 |
| 29 | 22767 | Мархал | нет | кафе | город Москва, Ангарская улица, дом 1, корпус 2 | 120 |
# Обратим внимание на тип данных в полях таблицы
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 15366 entries, 0 to 15365 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 15366 non-null int64 1 object_name 15366 non-null object 2 chain 15366 non-null object 3 object_type 15366 non-null object 4 address 15366 non-null object 5 number 15366 non-null int64 dtypes: int64(2), object(4) memory usage: 720.4+ KB
# Познакомимся с пропусками в данных.
df.isna().sum()
# Проверим таблицу на наличие грубых дубликатов
df.duplicated().sum()
0
Пропусков в данных и дубликатов нет, а вот тип поля chain, которое указывает на то, является ли ресторан сетевым или нет, можем заменить с object на boolean.
# Убедимся, что поле 'chain' действительно содержит два варианта значений
df['chain'].value_counts()
нет 12398 да 2968 Name: chain, dtype: int64
### КОД РЕВЬЮЕРА
df['chain'].replace({'да':True,'нет':False}).value_counts()
False 12398 True 2968 Name: chain, dtype: int64
# Для замены значений будем использовать конструкцию с атрибутом loc
df.loc[df['chain'] == 'да', 'chain'] = True
df.loc[df['chain'] == 'нет', 'chain'] = False
# Приведём тип столбца к типу boolean
df['chain'] = df['chain'].astype('boolean')
df.info() # Убедимся, что изменения вступили в силу
<class 'pandas.core.frame.DataFrame'> RangeIndex: 15366 entries, 0 to 15365 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 15366 non-null int64 1 object_name 15366 non-null object 2 chain 15366 non-null boolean 3 object_type 15366 non-null object 4 address 15366 non-null object 5 number 15366 non-null int64 dtypes: boolean(1), int64(2), object(3) memory usage: 630.4+ KB
# Также познакомимся с данными в конце таблицы
df.tail(30)
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 15336 | 211209 | Готовые блюда Милти | True | предприятие быстрого обслуживания | город Москва, Ореховый бульвар, дом 22А | 0 |
| 15337 | 208477 | Милти | True | предприятие быстрого обслуживания | город Москва, 3-й Крутицкий переулок, дом 18 | 0 |
| 15338 | 211201 | Милти | True | магазин (отдел кулинарии) | город Москва, Новослободская улица, дом 4 | 0 |
| 15339 | 210446 | Мята Lounge | True | кафе | город Москва, Большой Спасоглинищевский переул... | 38 |
| 15340 | 218067 | Мята lounge | True | кафе | город Москва, улица Крымский Вал, дом 10А, соо... | 74 |
| 15341 | 211269 | Лаундж-бар «Мята Lounge» | True | бар | город Москва, Днепропетровская улица, дом 2Б | 45 |
| 15342 | 210776 | Мята Lounge | True | ресторан | город Москва, город Московский, улица Хабарова... | 50 |
| 15343 | 211767 | Бар Мята Lounge | True | бар | город Москва, Митинская улица, дом 16 | 5 |
| 15344 | 206756 | Мята Lounge Октябрьская | True | кафе | город Москва, Донская улица, дом 11, строение 2 | 48 |
| 15345 | 205245 | Мята Lounge | True | кафе | город Москва, Бутырская улица, дом 67, строение 1 | 52 |
| 15346 | 208599 | Мята Lounge | True | кафе | город Москва, Куликовская улица, дом 1А | 30 |
| 15347 | 222491 | Кальянная «Мята Lounge» | True | кафе | город Москва, Профсоюзная улица, дом 142, корп... | 40 |
| 15348 | 212216 | Мята Lounge | True | кафе | город Москва, Привольная улица, дом 11 | 56 |
| 15349 | 206341 | Мята Lounge | True | кафе | город Москва, Салтыковская улица, дом 7Г | 100 |
| 15350 | 213061 | Мята | True | кафетерий | город Москва, Каширское шоссе, дом 96, корпус 1 | 35 |
| 15351 | 223036 | Якитория | True | ресторан | город Москва, Авиационная улица, дом 66 | 92 |
| 15352 | 213602 | Тануки | True | кафе | город Москва, Привольная улица, дом 65/32 | 50 |
| 15353 | 213772 | Тануки | True | ресторан | город Москва, Осенний бульвар, дом 9 | 98 |
| 15354 | 210400 | Шоколадница | True | кафе | город Москва, Театральный проезд, дом 5, строе... | 45 |
| 15355 | 74972 | Шоколадница | True | кафе | город Москва, улица Новый Арбат, дом 13 | 30 |
| 15356 | 220618 | Шоколадница | True | кафе | город Москва, Митинская улица, дом 36, корпус 1 | 100 |
| 15357 | 218692 | Шоколадница | True | кафе | город Москва, площадь Джавахарлала Неру, дом 1 | 30 |
| 15358 | 213724 | Шоколадница | True | кафе | город Москва, Варшавское шоссе, дом 87Б | 54 |
| 15359 | 222077 | Кофейня «Шоколадница» | True | кафе | город Москва, Кантемировская улица, дом 47 | 72 |
| 15360 | 219759 | Шоколадница | True | кафе | город Москва, улица Вавилова, дом 3 | 36 |
| 15361 | 208537 | Шоколадница | True | кафе | город Москва, 3-й Крутицкий переулок, дом 18 | 50 |
| 15362 | 209264 | Шоколадница | True | кафе | город Москва, улица Земляной Вал, дом 33 | 10 |
| 15363 | 209186 | Шоколадница | True | кафе | город Москва, улица Земляной Вал, дом 33 | 20 |
| 15364 | 221900 | Шоколадница | True | кафе | город Москва, поселение Московский, Киевское ш... | 36 |
| 15365 | 222535 | Шоколадница | True | кафе | город Москва, Ходынский бульвар, дом 4 | 10 |
Теперь, когда тип данных в столбцах таблицы нас устраивает, обратим внимание на значения в столбце object_name — они написаны в разных регистрах, а некоторые названия сетевых кафе имеют неявные дубликаты, например "Милти", "МИЛТИ" "Магазин готовой еды «Милти»" или "Мята Lounge" и "Лаундж-бар Мята Lounge". Поработаем с неявными дубликатами и начнём с названий, написанных заглавными буквами.
df['object_name'].value_counts() # Сейчас у нас 10393 разных названия для заведений питания
Столовая 267
Кафе 236
Шаурма 234
KFC 155
Шоколадница 142
...
Ресторан Панч энд Джуди 1
СУШИ МАГИЯ 1
Piu del Cibo 1
Ляфантази 1
Шашлыкян 1
Name: object_name, Length: 10393, dtype: int64
### КОД РЕВЬЮЕРА
import string
display(df.object_name.str.capitalize().unique()[:10])
word_list = []
(df.object_name
.str.lower() # понизили регистр
.str.translate(str.maketrans('', '', string.punctuation + '№«»')) # убрали пунктуацию
.drop_duplicates().str.split() # разбили на слова
.apply(word_list.extend) # добавили все слова в список
)
print(pd.Series(word_list).value_counts().head(50).index.to_list()) # вывели самые частотные
array(['Сметана', 'Родник', 'Кафе «академия»', 'Пиццетория',
'Кафе «вишневая метель»', 'Стол. при гоу сош № 1051', 'Брусника',
'Буфет мтуси', 'Кпф семья-1', 'Столовая мтуси'], dtype=object)
['кафе', 'столовая', 'ресторан', 'при', 'школа', 'бар', 'гбоу', 'буфет', 'и', 'сош', 'пицца', 'кофе', 'шк', 'школе', 'на', 'bar', 'суши', 'школы', 'пекарня', 'гоу', 'в', '1', 'кальянная', 'кофейня', 'колледж', 'кухня', 'lounge', 'coffee', 'им', 'cafe', 'клуб', 'питания', 'паб', 'шаурма', 'кдп', 'пиццерия', 'гриль', 'комбинат', 'чайхана', 'дворик', 'дом', 'донер', 'кп', 'мгу', 'хаус', 'цо', 'быстрого', 'чайхона', 'обслуживания', 'выпечка']
# Создадим список из названий, которые менять не нужно
reserved_list = ['KFC', 'КОМБИНАТ ПИТАНИЯ МГТУ ИМ.Н.Э.БАУМАНА']
def change_uppercase_names(object_name):
if object_name in reserved_list: # Если название в списке названий, которые мы не собираемся менять
return object_name # вернём исходное значение
if object_name.isupper(): # Если название содержит только заглавные символы
string = '' # то создадим пустую строку, в которую соберём исправленное название
name_list = object_name.split(' ') # Создадим список, который в качестве элементов будет содержать слова из названия
if len(name_list) == 1: # Рассмотрим случай, когда название состоит из одного слова
string += object_name[0].upper() # Строка начнётся с заглавного символа
string += object_name[1:].lower() # А продолжится строчными
return string # Функция вернёт строку
for i in range(len(name_list)): # Рассмотрим случай, когда название состоит из нескольких слов
string += name_list[i][0].upper()
string += name_list[i][1:].lower()
if i != len(name_list) - 1:
string += ' '
return string
return object_name # Иначе функция вернёт исходное значение
df['object_name'] = df['object_name'].apply(change_uppercase_names)
df['object_name'].value_counts()
Столовая 319
Кафе 277
Шаурма 250
Шоколадница 158
KFC 155
...
Пивной Бар «РЯБИНКА» 1
Бизес-центр Карачарово 1
Джанус 1
Bp 1
Шашлыкян 1
Name: object_name, Length: 10186, dtype: int64
Было 10393 вариантов названия заведений общепита, стало 10186 — мы избавились от более, чем 200 дубликатов. Теперь обратимся к сетевым заведениям и удалим скрытые дубликаты в их названиях.
# Посмотрим на первые 50 наиболее часто встречающихся названий сетевых заведений общественного питания
df.query('chain == True')['object_name'].value_counts().head(50)
Шоколадница 157 KFC 155 Макдоналдс 151 Бургер Кинг 133 Теремок 94 Домино'с Пицца 87 Крошка Картошка 83 Милти 72 Суши Wok 65 Папа Джонс 51 Кофе с собой 44 Чайхона №1 43 Якитория 38 Хинкальная 38 Кофе Хаус 35 Subway 34 Додо Пицца 34 Тануки 32 Starbucks 30 Иль Патио 25 Cofix 25 Хлеб насущный 24 Мята Lounge 23 Прайм стар 22 Старбакс 21 Сабвей 19 Пицца Хат 17 Пицца Паоло 15 Алло Пицца 15 Му-Му 15 Кофемания 15 Кулинарная лавка братьев Караваевых 15 СушиШоп 15 Кружка 14 Кафе «Шоколадница» 14 Штолле 13 Андерсон 13 Волконский 12 Баскин Роббинс 12 Суши Вок 11 Кафе «KFC» 11 СтардогS 11 Джон Джоли 10 Додо пицца 10 Вареничная №1 10 Прайм 10 Суши Сет 10 Грабли 10 Ваби-Саби 10 Братья Караваевы 9 Name: object_name, dtype: int64
Видно, что уже в этих 50 вариантах есть три неявных дубликата, которые связаны с набором символов, принадлежащих к различным раскладкам (и ещё больше дубликатов, связанных с регистром, а также с добавлением слов вроде слова "кафе"): Subway и Сабвей, Starbucks и Старбакс, Суши Wok и Cуши Вок. Поскольку варианты, написанные с использованием букв латинской раскладки клавиатуры, популярнее — заменим Сабвей на Subway, Cтарбакс на Starbucks, а Cуши Вок на Суши Wok и снова оценим 50 наиболее популярных вариантов.
# Будем заменять значения, обращаясь к ним через атрибут loc
df.loc[df['object_name'] == 'Старбакс', 'object_name'] = 'Starbucks'
df.loc[df['object_name'] == 'Сабвей', 'object_name'] = 'Subway'
df.loc[df['object_name'] == 'Суши Вок', 'object_name'] = 'Суши Wok'
df.query('chain == True')['object_name'].value_counts().head(50)
Шоколадница 157 KFC 155 Макдоналдс 151 Бургер Кинг 133 Теремок 94 Домино'с Пицца 87 Крошка Картошка 83 Суши Wok 76 Милти 72 Subway 53 Starbucks 51 Папа Джонс 51 Кофе с собой 44 Чайхона №1 43 Хинкальная 38 Якитория 38 Кофе Хаус 35 Додо Пицца 34 Тануки 32 Cofix 25 Иль Патио 25 Хлеб насущный 24 Мята Lounge 23 Прайм стар 22 Пицца Хат 17 Кулинарная лавка братьев Караваевых 15 Пицца Паоло 15 Му-Му 15 Кофемания 15 СушиШоп 15 Алло Пицца 15 Кружка 14 Кафе «Шоколадница» 14 Штолле 13 Андерсон 13 Волконский 12 Баскин Роббинс 12 Кафе «KFC» 11 СтардогS 11 Додо пицца 10 Джон Джоли 10 Вареничная №1 10 Прайм 10 Грабли 10 Ваби-Саби 10 Суши Сет 10 Воккер 9 Корчма Тарас Бульба 9 Му-му 9 То Да Сё 9 Name: object_name, dtype: int64
Subway, Starbucks и Суши Wok поднялись выше в отсортированном списке, значит всё сработало, как нужно. Заметим, что когда частота упоминаний названия заведения общественного питания становится ниже числа 15, начинают встречаться дубликаты, связанные с регистром, в котором написаны названия, а также дубликаты, связанные с добавлением дополнительных слов, вроде слова "Кафе". Создадим список из уникальных названий заведений, в этом списке окажутся те названия, частота упоминаний которых будет не меньше пятнадцати. Это первые 30 записей и ещё одна.
# Сохраним список из 31 элемента в переменной unique_names
unique_names = list(df.query('chain == True')['object_name'].value_counts().head(31).index)
# Напишем функцию, которая обработает дубликаты в названиях, связанные с разным регистром и добавлением дополнительных слов
def change_register_names(object_name):
for i in unique_names: # Запускаем цикл по списку популярных уникальных названий
if i.lower() in object_name.lower(): # Если очередной элемент из списка содержится в варианте названия заведения
return i # Заменяем такой вариант названия на название из списка
if 'старбакс' in object_name.lower(): # Также не забудем сделать замены в случаях, если в названии содержатся строки
# с популярными дубликатами, с теми, с которыми мы уже работали
return 'Starbucks'
if 'сабвей' in object_name.lower():
return 'Subway'
if 'суши вок' in object_name.lower():
return 'Суши Wok'
return object_name
# Применим функцию
df['object_name'] = df['object_name'].apply(change_register_names)
# Убедимся, что функция сработала
df.query('chain == True')['object_name'].value_counts().head(50)
KFC 188 Шоколадница 185 Макдоналдс 173 Бургер Кинг 159 Теремок 111 Домино'с Пицца 99 Крошка Картошка 96 Суши Wok 90 Милти 81 Starbucks 71 Папа Джонс 67 Subway 58 Додо Пицца 54 Хинкальная 54 Чайхона №1 52 Якитория 50 Кофе с собой 49 Тануки 47 Иль Патио 42 Кофе Хаус 41 Прайм стар 41 Хлеб насущный 33 Мята Lounge 33 Му-Му 32 Кулинарная лавка братьев Караваевых 25 Cofix 25 Пицца Хат 24 СушиШоп 17 Пицца Паоло 17 Кофемания 17 Алло Пицца 16 Кружка 14 Штолле 13 Андерсон 13 Волконский 12 Баскин Роббинс 12 СтардогS 11 Суши Сет 10 Вареничная №1 10 Прайм 10 Джон Джоли 10 Грабли 10 Ваби-Саби 10 То Да Сё 9 Корчма Тарас Бульба 9 Братья Караваевы 9 Французская выпечка 9 Воккер 9 Брусника 8 Джаганнат 7 Name: object_name, dtype: int64
# Оценим, сколько теперь разных вариантов названий для заведений содержится в столбце 'object_name'
df['object_name'].value_counts()
Столовая 319
Кафе 277
Шаурма 250
KFC 190
Шоколадница 189
...
Bp 1
CityLunch 1
Шервуд 1
Кафе-бар «Хижина» 1
Шашлыкян 1
Name: object_name, Length: 9914, dtype: int64
Было 10186 вариантов названия заведений общепита, стало 9914 — мы избавились ещё от почти 300 дубликатов. Продолжим работать с дубликатами в сетевых кафе и обратим внимание на те названия сетевых кафе, которые представлены в единственном экземпляре. Будем считать, что если заведение общественного питания сетевое, то его название должно упоминаться как минимум дважды, а значит названия с единственным упоминанием нуждаются в дополнительном исследовании.
# Сохраним в таблицу chain_names названия сетевых заведений общественного питания
chain_names = df.query('chain == True')['object_name'].value_counts()
# Сделаем индексы отдельным столбцом
chain_names = chain_names.reset_index()
# Присвоим столбцам новые имена
chain_names.columns = ['name', 'quantity']
# Перезапишем значение таблицы chain_names, оставив в ней только те названия, которые встречаются один раз
chain_names = chain_names.query('quantity == 1')
# Выведем на экран первые 50 строк получившейся таблицы
chain_names.iloc[0:].head(50)
| name | quantity | |
|---|---|---|
| 196 | Восточный базар | 1 |
| 197 | Tokyo bay | 1 |
| 198 | Тайм Авеню | 1 |
| 199 | Шантимель (кондитерские) | 1 |
| 200 | Закусочная «Баскин Роббинс & Стардогс» | 1 |
| 201 | Кафе ПРОНТО | 1 |
| 202 | Кафе «То Да Сё» | 1 |
| 203 | Фитнес-бар «world Class» | 1 |
| 204 | Японский ресторан «Ваби-Саби» | 1 |
| 205 | Кулинарное Бюро | 1 |
| 206 | Пицца Pomodoro и Суши Дзен | 1 |
| 207 | Кафе при АЗС | 1 |
| 208 | Шварцвальд | 1 |
| 209 | Лукойл | 1 |
| 210 | АВ-Дейли Азбука Вкуса | 1 |
| 211 | КАФЕ Кофе-Хаус | 1 |
| 212 | Ресторан Джон Джоли | 1 |
| 213 | Кафе Штолле | 1 |
| 214 | Корчма «тарас Бульба» | 1 |
| 215 | Грузинские каникулы Барбарис | 1 |
| 216 | Пиццерия Донателло | 1 |
| 217 | Ян Примус | 1 |
| 218 | Кафе «Урюк» Хивинская чайхона | 1 |
| 219 | IL Forno Иль Форно | 1 |
| 220 | Домашнее Кафе сеть городских кафе | 1 |
| 221 | Перекресток | 1 |
| 222 | Кафе «Кулинарное бюро» | 1 |
| 223 | Урожай | 1 |
| 224 | Пироги Штолле | 1 |
| 225 | КАФЕ «Андерсон» | 1 |
| 226 | Maki Maki | 1 |
| 227 | Шоколад | 1 |
| 228 | Кафе при АЗС Газпромнефть | 1 |
| 229 | Ресторан «Ян Примус» | 1 |
| 230 | Виват-Пицца | 1 |
| 231 | Чешская пивная ПИЛЗНЕР | 1 |
| 232 | Panda Express | 1 |
| 233 | СтардогS и шаурма | 1 |
| 234 | Тратория Semplice | 1 |
| 235 | Torro Grill Торро Гриль | 1 |
| 236 | Ресторан «Вьеткафе» | 1 |
| 237 | Бир Хаус Паб | 1 |
| 238 | БРАВА Коста кофе | 1 |
| 239 | ресторан «Брудер» | 1 |
| 240 | КОФЕЙНЯ «Costa Coffee» | 1 |
| 241 | Бар-буфет Николай | 1 |
| 242 | BooBo | 1 |
| 243 | Black & White | 1 |
| 244 | В&В Бургер | 1 |
| 245 | Гино-но-таки | 1 |
# Посчитаем количество записей в таблице chain_names
len(chain_names)
261
Теперь напишем функцию, которая поможет избавиться от дополнительных уточняющих слов, стоящих в начале. Будем с помощью такой функции оставлять названия, написанные внутри кавычек-ёлочек, удаляя эти кавычки-ёлочки.
# Сохраним в переменной wrong_names_list список значений столбца 'name' таблицы chain_names
wrong_names_list = list(chain_names['name'])
def change_chain_names(object_name):
for i in wrong_names_list: # Запустим цикл по списку wrong_names_list
if object_name == i: # Если очередное значение из object_name равно очередному элементу списка wrong_names_list
if object_name.find("«") > -1: # И если в очередном значении из object_name содержится символ "«"
new_value = object_name.split("«")[1].rstrip("»") # Разделим по "«", обратимся ко второму элементу и избавимся от "»" в конце
if new_value.upper() == new_value or new_value.lower() == new_value: # Если new_value написанно только в верхнем или только в нижнем регистре
string = '' # Создадим пустую строку, в которую соберём новое название
name_list = new_value.split(' ') # Создадим список, который в качестве элементов будет содержать слова из названия
if len(name_list) == 1: # Рассмотрим случай, когда название состоит из одного слова
string += new_value[0].upper() # Строка начнётся с заглавного символа
string += new_value[1:].lower() # А продолжится строчными
return string # Функция вернёт строку
for i in range(len(name_list)): # Рассмотрим случай, когда название состоит из нескольких слов
string += name_list[i][0].upper()
string += name_list[i][1:].lower()
if i != len(name_list) - 1: # Если это не последнее слово в названии
string += ' ' # Тогда добавляем пробел после не последнего слова
return string
return new_value
return object_name # Иначе функция вернёт исходное значение
df['object_name'] = df['object_name'].apply(change_chain_names)
# Сохраним в таблицу chain_names2 названия сетевых заведений общественного питания
chain_names2 = df.query('chain == True')['object_name'].value_counts()
# Сделаем индексы отдельным столбцом
chain_names2 = chain_names2.reset_index()
# Присвоим столбцам новые имена
chain_names2.columns = ['name', 'quantity']
# Перезапишем значение таблицы chain_names2, оставив в ней только те названия, которые встречаются один раз
chain_names2 = chain_names2.query('quantity == 1')
# Выведем на экран первые 50 строк получившейся таблицы
chain_names2.iloc[100:].head(50)
| name | quantity | |
|---|---|---|
| 305 | Нияма Пицца Пи | 1 |
| 306 | Стейк-хаус Гудман | 1 |
| 307 | Лукойл-Центрнефтьпродукт | 1 |
| 308 | Ариана | 1 |
| 309 | Городские автокофейни | 1 |
| 310 | Васаби | 1 |
| 311 | Марукамэ | 1 |
| 312 | Bierloga | 1 |
| 313 | Кафе ПРОНТО | 1 |
| 314 | Метро К&К | 1 |
| 315 | Виктория | 1 |
| 316 | Советские времена | 1 |
| 317 | Ванвок | 1 |
| 318 | Гурмания | 1 |
| 319 | ББ энд БУРГЕРС В&В Бургер | 1 |
| 320 | Оникс | 1 |
| 321 | Tajj Mahal | 1 |
| 322 | Мясоroob | 1 |
| 323 | Территория Ясенево | 1 |
| 324 | Grand Cru | 1 |
| 325 | VietCafe Вьеткафе | 1 |
| 326 | Кафе Космик | 1 |
| 327 | Deli by Prime Прайм-кафе | 1 |
| 328 | В&В Бургер | 1 |
| 329 | TGI Fridays | 1 |
| 330 | Van Wok Ванвок | 1 |
| 331 | Бакинский Бульвар | 1 |
| 332 | Советские времена Чебуречная СССР | 1 |
| 333 | Сити | 1 |
| 334 | Тамаси Суши | 1 |
| 335 | Траттория Примавера | 1 |
| 336 | Бир хаус | 1 |
| 337 | Пироговая Штолле | 1 |
| 338 | Мюнгер | 1 |
| 339 | Да Пино (Da Pino) | 1 |
| 340 | HEALTHY Food | 1 |
| 341 | Семейное кафе Андерсон | 1 |
| 342 | Кофешоп Coffeeshop Company | 1 |
| 343 | Cookhouse | 1 |
| 344 | Healthy food | 1 |
| 345 | The Terrace | 1 |
| 346 | Джардино Да Пино | 1 |
| 347 | Пивко | 1 |
| 348 | Кальянная F-lounge | 1 |
| 349 | Макс Бреннер | 1 |
| 350 | Upside Down | 1 |
| 351 | Travelers Coffe | 1 |
| 352 | Marmalato | 1 |
| 353 | Хачапури, Одесса -мама | 1 |
| 354 | Гудман Гудвин | 1 |
# Снова посчитаем количество записей в таблице chain_names2
len(chain_names2)
199
В результате работы функции change_chain_names() мы избавились от ещё 63 дубликатов в столбце 'object_name'. Продолжим работу с дубликатами и теперь будем избавляться от слов вроде «Кафе», «Ресторан», «Бар», которые стоят в начале. Также определим слова, наличие которых будет сообщать нам о необходимости замены имеющегося названия на это слово. Для этого создадим три списка: один для наиболее популярных уточняющих слов, другой — для слов-триггеров и ещё один — для значений столбца 'name' таблицы chain_names2.
# Сохраним в переменной wrong_names_list2 список значений столбца 'name' таблицы chain_names2
wrong_names_list2 = list(chain_names2['name'])
# Сохраним в переменной start_words список популярных уточняющих слов, стоящих перед названиями заведений общепита
start_words = ['Ресторан', 'Пиццерия', 'Траттория', 'Кальянная', 'Семейное кафе', 'Бистро', 'Кафе', 'Бар', 'БАР', 'Пицца',
'Пироговая', 'КАФЕ', 'Пироги', 'Паб', 'Кондитерия', 'АВ-Дейли']
# Сохраним в переменной signal_names список слов, наличие которых будем считать сигналом к замене названия на это слово
signal_words = ['Магбургер', 'СтардогS', 'Баскин Роббинс', 'Газпромнефть', 'Шантимель', 'Tutti Frutti', 'Шантимель', 'Нияма',
'IL Forno', 'White Rabbit', 'Лукойл', 'Торро Гриль', 'ИКЕА', 'Вьеткафе', 'Fridays', 'Джон Джоли', 'Штолле',
'Волконский', 'Гудман', 'Пронто', 'Пипони', 'Коста Кофе', 'Чешская Пивная Пилзнер', 'Зю', 'Да Пино',
'World Class', 'В&В Бургер', 'Урюк', 'Ваби-Саби']
def change_chain_names_2(object_name):
for i in wrong_names_list2: # Запустим цикл по списку wrong_names_list2
if object_name == i: # Если очередное значение из object_name равно очередному элементу списка wrong_names_list2
for j in signal_words: # Тогда запускаем цикл for по списку signal_words
if j.lower() in i.lower(): # Если очередной элемент из signal_words содержится в очередном элементе из wrong_names_list2 независимо от регистра
return j # Функция вернёт очередной элемент из signal_words
return object_name # Иначе фукнция вернёт исходное значение
df['object_name'] = df['object_name'].apply(change_chain_names_2)
chain_names3 = df.query('chain == True')['object_name'].value_counts()
chain_names3 = chain_names3.reset_index()
chain_names3.columns = ['name', 'quantity']
chain_names3 = chain_names3.query('quantity == 1')
# Посчитаем количество записей в таблице chain_names3
len(chain_names3)
146
# Сократили число дубликатов ещё на 45, ознакомимся с данными
chain_names3.iloc[0:].head(50)
| name | quantity | |
|---|---|---|
| 215 | IL Forno | 1 |
| 216 | Кебаб хаус | 1 |
| 217 | Ганс и Марта | 1 |
| 218 | Чайхана Тапчан | 1 |
| 219 | Крепери де Пари | 1 |
| 220 | Пиццерия Пиу дель Чибо | 1 |
| 221 | Братья Караваевых | 1 |
| 222 | Panda Express | 1 |
| 223 | Пражечка | 1 |
| 224 | Кафе КОФЕ ТУН | 1 |
| 225 | Бар Боулинг-Космик | 1 |
| 226 | Бар-буфет Николай | 1 |
| 227 | МясоROOB | 1 |
| 228 | Кулинарное бюро Китчен | 1 |
| 229 | БАР Азбука Вкуса | 1 |
| 230 | Генацвали | 1 |
| 231 | Молли гвинз | 1 |
| 232 | Территория TIMBIGFAMILY | 1 |
| 233 | Кондитерия Тирольские пироги | 1 |
| 234 | Хачапури, Одесса -мама | 1 |
| 235 | Бабай Клаб | 1 |
| 236 | Оникс | 1 |
| 237 | Шашлык-машлык | 1 |
| 238 | Билла | 1 |
| 239 | CoffeeShop | 1 |
| 240 | АВ-Дейли Азбука Вкуса | 1 |
| 241 | Кальян-бар MosKalyan | 1 |
| 242 | Помидор | 1 |
| 243 | HEALTHY Food | 1 |
| 244 | Барбарис | 1 |
| 245 | Movenpick | 1 |
| 246 | Ливан-Хаус | 1 |
| 247 | Милано пицца | 1 |
| 248 | Кулинарное бюро Kitchen | 1 |
| 249 | Торнадо | 1 |
| 250 | Перекресток | 1 |
| 251 | Праймстар | 1 |
| 252 | Гино-но-таки | 1 |
| 253 | Пиппони | 1 |
| 254 | Крепери де пари | 1 |
| 255 | МСК Московская сеть кальянных на Шаболовке | 1 |
| 256 | Пицца Pomodoro и Суши Дзен | 1 |
| 257 | Добрынинский и партнёры | 1 |
| 258 | Сувлаки | 1 |
| 259 | Бургер Клаб | 1 |
| 260 | Кафе Космик | 1 |
| 261 | Американ Сити Пицца | 1 |
| 262 | Семейное кафе Андерсон | 1 |
| 263 | Чешская Пивная Пилзнер | 1 |
| 264 | Простые Вещи | 1 |
# Сохраним в переменной wrong_names_list3 список значений столбца 'name' таблицы chain_names2
wrong_names_list3 = list(chain_names3['name'])
def change_chain_names_3(object_name):
for i in wrong_names_list3: # Запустим цикл по списку wrong_names_list3
if object_name == i: # Если очередное значение из object_name равно очередному элементу списка wrong_names_list3
for j in start_words: # Тогда запускаем цикл for по списку start_words
if i.find(j) > -1: # Если очередной элемент из списка start_words содержится в очередном значении из wrong_names_list3
new_value = object_name.split(j)[0] # Тогда удалим это значение
return new_value
return object_name
df['object_name'] = df['object_name'].apply(change_chain_names_3)
chain_names4 = df.query('chain == True')['object_name'].value_counts()
chain_names4 = chain_names4.reset_index()
chain_names4.columns = ['name', 'quantity']
chain_names4 = chain_names4.query('quantity == 1')
# Посчитаем количество записей в таблице chain_names4
len(chain_names4)
121
chain_names4.iloc[0:].head(10)
| name | quantity | |
|---|---|---|
| 217 | Сказка | 1 |
| 218 | Крепери де Пари | 1 |
| 219 | Чайхана Тапчан | 1 |
| 220 | Праймстар | 1 |
| 221 | Кулинарное бюро Kitchen | 1 |
| 222 | Милано пицца | 1 |
| 223 | Кулинарное бюро Китчен | 1 |
| 224 | Перекресток | 1 |
| 225 | Гино-но-таки | 1 |
| 226 | Пиппони | 1 |
Мы избавились от 140 дубликатов в названиях сетевых заведений общественного питания. Оставшиееся 121 название можно удалить, руководствуясь принципом "Сетевые заведения — это те заведения, количество которых больше одного".
# Сохранили 121 название в список
wrong_names = list(chain_names4['name'])
# Напишем функцию, которая подготовит строки с такими названиями к удалению
def prepare_names_for_delete(object_name):
for i in wrong_names:
if object_name == i:
return 'delete'
return object_name
# Применим функцию
df['object_name'] = df['object_name'].apply(prepare_names_for_delete)
# Оставим в таблице только те строки, которые не содержат в столбце 'object_name' строк 'delete'
df = df[df['object_name'] != 'delete']
# Убедимся, что удаление строк со значением 'delete' прошло успешно: этого значения не должно быть среди топ-10 по популярности
df['object_name'].value_counts().head(10)
Столовая 319 Кафе 277 Шаурма 250 KFC 190 Шоколадница 189 Макдоналдс 173 Бургер Кинг 161 Теремок 116 Домино'с Пицца 99 Крошка Картошка 96 Name: object_name, dtype: int64
# Оценим количество уникальных значений столбца 'object_name' после работы с дубликатами
df['object_name'].value_counts()
Столовая 319
Кафе 277
Шаурма 250
KFC 190
Шоколадница 189
...
Столовая При Гоу Сош № 1902 1
Пробка Гриль 1
BIG суши 1
Веранда 6.1 КРАБЫКУТАБЫ 1
Шашлыкян 1
Name: object_name, Length: 9669, dtype: int64
В результате проведённой работы с дубликатами в столбце 'object_name' мы устранили свыше 700 дублирующих названий у заведений общественного питания, что составляет чуть менее 10% от всех записей таблицы. Будем считать проведённую работу с дубликатами успешной.
df.head()
| id | object_name | chain | object_type | address | number | |
|---|---|---|---|---|---|---|
| 0 | 151635 | Сметана | False | кафе | город Москва, улица Егора Абакумова, дом 9 | 48 |
| 1 | 77874 | Родник | False | кафе | город Москва, улица Талалихина, дом 2/1, корпус 1 | 35 |
| 2 | 24309 | Кафе «Академия» | False | кафе | город Москва, Абельмановская улица, дом 6 | 95 |
| 3 | 21894 | Пиццетория | True | кафе | город Москва, Абрамцевская улица, дом 1 | 40 |
| 4 | 119365 | Кафе «Вишневая метель» | False | кафе | город Москва, Абрамцевская улица, дом 9, корпус 1 | 50 |
Исследуем соотношение видов объектов общественного питания по количеству.
# Для представления соотношения видов объектов общественного питания по количеству построим сводную таблицу
kind_df = df.pivot_table(index='object_type', values='id', aggfunc='count')
kind_df.columns = ['quantity']
kind_df['kind'] = kind_df.index
kind_df.sort_values(by='quantity', ascending=False, inplace=True)
kind_df.reset_index(drop=True, inplace=True)
fig = px.bar(
kind_df, x='kind', y='quantity', title='Соотношение видов объектов общественного питания по количеству',
labels = {'quantity': 'количество', 'kind':'тип заведения'},
color_discrete_sequence=['NavajoWhite']
)
fig.show()
На графике видно, что наиболее распространённый тип заведений общественного питания в Москве — это кафе: 6028 заведений. На втором по популярности месте с существенным отрывом (2587 заведений) следует тип «столовая» и замыкает тройку лидеров тип «ресторан» с 2245 заведениями. Наименее популярный тип заведений общественного питания — это «магазин (отдел кулинарии)», таких всего 268.
Теперь исследуем соотношение сетевых и несетевых заведений по количеству, построим новый график.
# Как и в прошлом случае, можем воспользоваться сводной таблицей
chain_df = df.pivot_table(index='chain', values='id', aggfunc='count')
chain_df.columns = ['quantity']
chain_df['is_chain'] = chain_df.index
chain_df.reset_index(drop=True, inplace=True)
fig = px.bar(
chain_df, x='is_chain', y='quantity', title='Соотношение сетевых и несетевых заведений по количеству',
labels={'quantity': 'количество'},
color_discrete_sequence=['Plum']
)
fig.show()
Несетевых заведений общественного питания в Москве больше, чем сетевых. Разница примерно в 5 раз. Такое соотношение даёт нам больше уверенности в том, что существование небольшого несетевого кафе более чем возможно на рынке московских заведений, ведь таких несетевых заведений большинство.
Теперь обратим внимание на то, для какого вида объектов общественного питания характерно сетевое распространение.
# Снова обратимся к сводной таблице. Тип данных boolean в столбце chain позволяет нам очень удобно посчитать долю сетевых
# объектов общепита для каждого из типов объектов
character_chain_dist = df.pivot_table(index='object_type', values='chain', aggfunc='mean')
character_chain_dist.columns = ['part']
# Округлим значения столбца 'part' до трёх знаков после точки и приведём их к процентному виду для удобства восприятия
character_chain_dist['part'] = character_chain_dist['part'].astype('float').round(3) * 100
character_chain_dist['object_type'] = character_chain_dist.index
character_chain_dist.reset_index(drop=True, inplace=True)
character_chain_dist.sort_values(by='part', ascending=False, inplace=True)
character_chain_dist
fig = px.bar(
character_chain_dist, x='object_type', y='part',
title = 'Доля сетевого распространения для типов объектов общественного питания',
labels = {'part': '%', 'object_type': 'тип объекта'},
color_discrete_sequence=['Aquamarine']
)
fig.show()
Абсолютным лидером по сетевому распространению явлются объекты типа «Предприятие быстрого обслуживания» — 41,1% таких предприятий сетевые, показатель для кафе (ведь нас интересует именно кафе) — 22.2%, то есть 77.8% кафе сетевыми не являются. Это хорошие новости для нас. Также заметим, что только десятая доля процента столовых относятся к сетевым, а количество заведений типа «столовая», напомним, находится на втором месте по популярности в соотношении объектов по типу. Вероятно, принцип "Вместе выжить проще" не распространяется на столовые и они не страдают от дефицита клиентов, поэтому лишь 0,1% столовых объединяется в сети, а остальные существуют автономно.
Теперь попробуем ответить на вопрос: Что характерно для сетевых заведений: много заведений с небольшим числом посадочных мест в каждом или мало заведений с большим количеством посадочных мест? Для ответа на этот вопрос сперва категоризируем заведения по количеству посадочных мест.
# Изучим данные столбца 'number' с помощью метода describe()
df['number'].describe()
count 15217.000000 mean 59.600775 std 74.854843 min 0.000000 25% 12.000000 50% 40.000000 75% 80.000000 max 1700.000000 Name: number, dtype: float64
# Сохраним значения медианы в переменной q2: это значение примем за границу,
# по которой разделим заведения по числу посадочных мест на категории вместимости: 'мало мест' и 'много мест'
q2 = int(df['number'].describe()[5])
# Напишем функцию для создания категорий
def make_capacity_type(number):
if number <= q2:
return 'мало мест'
return 'много мест'
df['capacity_type'] = df['number'].apply(make_capacity_type)
# Убедимся, что применение функции дало результат
df.head()
| id | object_name | chain | object_type | address | number | capacity_type | |
|---|---|---|---|---|---|---|---|
| 0 | 151635 | Сметана | False | кафе | город Москва, улица Егора Абакумова, дом 9 | 48 | много мест |
| 1 | 77874 | Родник | False | кафе | город Москва, улица Талалихина, дом 2/1, корпус 1 | 35 | мало мест |
| 2 | 24309 | Кафе «Академия» | False | кафе | город Москва, Абельмановская улица, дом 6 | 95 | много мест |
| 3 | 21894 | Пиццетория | True | кафе | город Москва, Абрамцевская улица, дом 1 | 40 | мало мест |
| 4 | 119365 | Кафе «Вишневая метель» | False | кафе | город Москва, Абрамцевская улица, дом 9, корпус 1 | 50 | много мест |
### КОД РЕВЬЮЕРА
import numpy as np
print(df['number'].quantile(0.5))
(np.where(df['number'] > df['number'].median(),'много мест','мало мест') != df.capacity_type.values).sum()
40.0
0
Теперь, когда мы категоризировали заведения общественного питания по вместимости, разделив их на две группы "много мест" и "мало мест", категоризируем сетевые заведения по количеству точек в сети. У нас появится две категории: "крупная сеть" и "малая сеть".
# Создадим отдельную таблицу для сетевых заведений общественного питания
chain_df = df.query('chain == True')
# Изучим данные
chain_df['object_name'].value_counts().describe()
count 217.000000 mean 13.119816 std 28.780852 min 2.000000 25% 2.000000 50% 3.000000 75% 8.000000 max 188.000000 Name: object_name, dtype: float64
# Сохраним в переменной q2 значение медианы, по которой разделим сети на группы
q2 = int(chain_df['object_name'].value_counts().describe()[5])
chains_and_quantity = chain_df.pivot_table(index='object_name', values='id', aggfunc='count')
chains_and_quantity.columns = ['quantity']
chains_and_quantity['chain'] = chains_and_quantity.index
chains_and_quantity.reset_index(drop=True, inplace=True)
chains_and_quantity.sort_values(by='quantity', ascending=False, inplace=True)
chains_and_quantity = chains_and_quantity[['chain', 'quantity']]
# Сохраним в список названия крупных сетей. За крупную сеть будем считать сеть с количеством заведений большим, чем медиана
big_chain_list = list(chains_and_quantity.query('quantity > @q2')['chain'])
len(big_chain_list) # Количество таких сетей равно числу 101
101
# Теперь добавим в таблицу chain_df столбец с категорией сети "крупная сеть" или "малая сеть"
def make_chain_type(object_name):
if object_name in big_chain_list:
return 'крупная сеть'
return 'малая сеть'
chain_df['chain_type'] = chain_df['object_name'].apply(make_chain_type)
# Убедимся, что столбец с типом сети был успешно добавлен
chain_df.head()
| id | object_name | chain | object_type | address | number | capacity_type | chain_type | |
|---|---|---|---|---|---|---|---|---|
| 3 | 21894 | Пиццетория | True | кафе | город Москва, Абрамцевская улица, дом 1 | 40 | мало мест | малая сеть |
| 6 | 148815 | Брусника | True | кафе | город Москва, переулок Сивцев Вражек, дом 6/2 | 10 | мало мест | крупная сеть |
| 13 | 22579 | Алло Пицца | True | кафе | город Москва, улица Авиаторов, дом 14 | 32 | мало мест | крупная сеть |
| 16 | 144107 | Суши Wok | True | предприятие быстрого обслуживания | город Москва, Азовская улица, дом 3 | 7 | мало мест | крупная сеть |
| 18 | 58565 | Тануки | True | ресторан | город Москва, Большая Академическая улица, дом 65 | 160 | много мест | крупная сеть |
Наконец, мы подходим вплотную к вопросу «Что характерно для сетевых заведений: много заведений с небольшим числом посадочных мест в каждом или мало заведений с большим количеством посадочных мест?». Построим графики на основе сводной таблицы
character_chain_capa = chain_df.pivot_table(index='capacity_type', values='id', aggfunc='count')
character_chain_capa.columns = ['quantity']
character_chain_capa['capa_type'] = character_chain_capa.index
character_chain_capa.reset_index(drop=True, inplace=True)
character_chain_capa = character_chain_capa[['capa_type', 'quantity']]
fig = px.bar(
character_chain_capa, x='capa_type', y='quantity', title='Количество сетевых заведений по типу вместимости',
labels = {'quantity': 'количество заведений', 'capa_type': 'тип вместимости'},
color_discrete_sequence=['PaleGreen']
)
fig.show()
# Также рассмотрим, как ведёт себя тип вместимости в заведениях, принадлежащих крупным сетям и малым сетям
character_type_chain_capa = chain_df.pivot_table(index='capacity_type', values='id', columns='chain_type', aggfunc='count')
character_type_chain_capa.columns = ['big_chain', 'small_chain']
character_type_chain_capa['capa_type'] = character_type_chain_capa.index
character_type_chain_capa.reset_index(drop=True, inplace=True)
character_type_chain_capa = character_type_chain_capa[['capa_type', 'small_chain', 'big_chain']]
fig = px.pie(character_type_chain_capa, values='small_chain', names='capa_type',
title='Доля заведений, принадлежащих к малым сетям по количеству мест:',
color_discrete_sequence=['LightSkyBlue', 'PaleTurquoise'],
labels={'capa_type': 'тип вместимости', 'small_chain': 'количество заведений'}
)
fig.show()
fig = px.pie(character_type_chain_capa, values='big_chain', names='capa_type',
title='Доля заведений, принадлежащих к крупным сетям по количеству мест:',
color_discrete_sequence=['PaleTurquoise','LightSkyBlue'],
labels={'capa_type': 'тип вместимости', 'big_chain': 'количество заведений'}
)
fig.show()
Наиболее характерным признаком сетевых заведений является большое количество заведений с небольшим количеством посадочных мест, однако это не отменяет факта существования заведений с большим количеством мест, количество которых чуть меньше. Характерный признак проявляет себя в основном в заведениях, которые принадлежат к крупным сетям: 54.5 % таких заведений имеют малое число мест. В заведениях, принадлежащих малым сетям, напротив, чаще можно встретить большое количество мест — в 53.4 % случаев. Но поскольку заведений, принадлежащих к крупным сетям значимо больше (почти в 10 раз!), то мы смело можем утверждать, что характерный признак сетевых заведений — небольшое количество посадочных мест в большом количестве сетевых заведений общественного питания.
Опишем среднее количество посадочных мест для каждой категории объекта общественного питания.
# Построим сводную таблицу на основе таблицы chain_df
average_seats_by_object_type = chain_df.groupby(['chain_type', 'capacity_type']).agg({'number':'mean'})
average_seats_by_object_type.columns = ['avg_seats']
average_seats_by_object_type['avg_seats'] = round(average_seats_by_object_type['avg_seats']).astype('int')
average_seats_by_object_type['chain_and_seat_type'] = [
'крупная сеть, мало мест', 'крупная сеть, много мест', 'малая сеть, мало мест', 'малая сеть, много мест']
average_seats_by_object_type.reset_index(drop=True, inplace=True)
average_seats_by_object_type = average_seats_by_object_type[['chain_and_seat_type', 'avg_seats']]
average_seats_by_object_type
| chain_and_seat_type | avg_seats | |
|---|---|---|
| 0 | крупная сеть, мало мест | 17 |
| 1 | крупная сеть, много мест | 93 |
| 2 | малая сеть, мало мест | 17 |
| 3 | малая сеть, много мест | 101 |
plt.figure(figsize=(16, 6))
a = plt.bar(average_seats_by_object_type['chain_and_seat_type'], average_seats_by_object_type['avg_seats'], color='thistle')
for rect, label in zip(a.patches, average_seats_by_object_type['avg_seats']):
plt.text(
rect.get_x() + rect.get_width() / 2,
rect.get_height(),
label,
ha='center',
va='bottom'
)
plt.title('Среднее количество посадочных мест по категориям заведений общественного питания')
plt.show()
В среднем самое большое количество посадочных мест мы видим у заведений, которые принадлежат к категории "малая сеть, много мест" — значение среднего числа мест равно 101. Второй по среднему количеству мест категорией объектов общественного питания является "крупная сеть, много мест". Средние показатели и крупной и малой сетей с малым количеством мест равны 17.
Выделим в отдельный столбец информацию об улице из столбца address.
# Вспомним, как выглядит таблица и обратим внимание на то, что информация об улице обычно отделена с двух сторон запятыми.
df.head(5)
| id | object_name | chain | object_type | address | number | capacity_type | |
|---|---|---|---|---|---|---|---|
| 0 | 151635 | Сметана | False | кафе | город Москва, улица Егора Абакумова, дом 9 | 48 | много мест |
| 1 | 77874 | Родник | False | кафе | город Москва, улица Талалихина, дом 2/1, корпус 1 | 35 | мало мест |
| 2 | 24309 | Кафе «Академия» | False | кафе | город Москва, Абельмановская улица, дом 6 | 95 | много мест |
| 3 | 21894 | Пиццетория | True | кафе | город Москва, Абрамцевская улица, дом 1 | 40 | мало мест |
| 4 | 119365 | Кафе «Вишневая метель» | False | кафе | город Москва, Абрамцевская улица, дом 9, корпус 1 | 50 | много мест |
# Напишем функцию для создания нового столбца с названием улицы
def extract_street_name(address):
if ',' in address:
if address.split(',')[1].strip() == 'город Зеленоград' or address.split(',')[1].strip() == 'поселение Сосенское':
return address.split(',')[1].strip() + ', ' + address.split(',')[2].strip()
return address.split(',')[1].strip()
return 'улица не определена'
df['street_name'] = df['address'].apply(extract_street_name)
# Убедимся, что функция сработала верно
df.head()
| id | object_name | chain | object_type | address | number | capacity_type | street_name | |
|---|---|---|---|---|---|---|---|---|
| 0 | 151635 | Сметана | False | кафе | город Москва, улица Егора Абакумова, дом 9 | 48 | много мест | улица Егора Абакумова |
| 1 | 77874 | Родник | False | кафе | город Москва, улица Талалихина, дом 2/1, корпус 1 | 35 | мало мест | улица Талалихина |
| 2 | 24309 | Кафе «Академия» | False | кафе | город Москва, Абельмановская улица, дом 6 | 95 | много мест | Абельмановская улица |
| 3 | 21894 | Пиццетория | True | кафе | город Москва, Абрамцевская улица, дом 1 | 40 | мало мест | Абрамцевская улица |
| 4 | 119365 | Кафе «Вишневая метель» | False | кафе | город Москва, Абрамцевская улица, дом 9, корпус 1 | 50 | много мест | Абрамцевская улица |
### КОД РЕВЬЮЕРА
print(df.street_name[df.street_name.apply(len).lt(9)].to_list())
['дом 30', 'дом 30', 'дом 5/14', 'дом 10', 'дом 26/1', 'дом 3', 'дом 4', 'дом 1', 'дом 118Б', 'дом 8А/5', 'дом 1/4', 'дом 88', 'дом 2', 'дом 23', 'дом 26', 'дом 26', 'дом 26', 'дом 11А', 'дом 8', 'дом 6', 'дом 11', 'дом 14', 'дом 48', 'дом 20', 'дом 28', 'дом 18', 'дом 26', 'дом 90', 'дом 11А', 'дом 46', 'дом 2', 'дом 16', 'дом 48', 'дом 37', 'дом 5', 'дом 39', 'дом 2/34', 'дом 1/4', 'дом 9', 'дом 7', 'дом 52А', 'дом 18', 'дом 18', 'дом 18', 'дом 7', 'дом 7', 'дом 7', 'дом 1/4', 'дом 88', 'дом 5']
# Напишем функцию, которая подготовит эти значения к удалению
def prepare_street_names_for_delete(street_name):
if len(street_name) < 9:
return 'delete_this_value'
return street_name
df['street_name'] = df['street_name'].apply(prepare_street_names_for_delete)
# Проверим, что их точно 50
len(df.query('street_name == "delete_this_value"'))
50
# Удалим значения
df = df.query('street_name != "delete_this_value"')
Построим график топ-10 улиц по количеству объектов общественного питания.Также ответим на вопрос — в каких районах Москвы находятся эти улицы. Начнём с построения сводной таблицы
# Построим сводную таблицу
popular_streets = df.pivot_table(index='street_name', values='id', aggfunc='count')
popular_streets.columns = ['points']
popular_streets['street_name'] = popular_streets.index
popular_streets.reset_index(drop=True, inplace=True)
# Поменяем последовательность столбцов
popular_streets = popular_streets[['street_name', 'points']]
# Отсортируем данные по убыванию столбца 'points'
popular_streets.sort_values(by='points', ascending=False, inplace=True)
# Запишем значение таблицы, с топ-10 в переменную most_popular_streets
most_popular_streets = popular_streets.head(10)
display(most_popular_streets)
| street_name | points | |
|---|---|---|
| 1559 | проспект Мира | 204 |
| 990 | Профсоюзная улица | 179 |
| 681 | Ленинградский проспект | 170 |
| 976 | Пресненская набережная | 167 |
| 405 | Варшавское шоссе | 161 |
| 684 | Ленинский проспект | 145 |
| 1556 | проспект Вернадского | 130 |
| 672 | Кутузовский проспект | 112 |
| 598 | Каширское шоссе | 111 |
| 603 | Кировоградская улица | 106 |
fig = px.bar(
most_popular_streets, x='street_name', y='points',
labels={'points': 'количество заведений', 'street_name': 'улица'},
title = 'Топ-10 улиц по количеству объектов общественного питания',
color_discrete_sequence=['LightSalmon']
)
fig.show()
# Сохраним в переменной url ссылку на csv-файл с внешними данными об округах и районах
url = 'https://frs.noosphere.ru/xmlui/bitstream/handle/20.500.11925/714058/mosgaz-streets.csv'
# Считаем таблицу в переменную districts_df
districts_df = pd.read_csv(url)
# Убедимся, что таблица сохранилась в переменной, прочитаем первые 5 строк этой таблицы
districts_df.head()
| streetname | areaid | okrug | area | |
|---|---|---|---|---|
| 0 | Выставочный переулок | 17 | ЦАО | Пресненский район |
| 1 | улица Гашека | 17 | ЦАО | Пресненский район |
| 2 | Большая Никитская улица | 17 | ЦАО | Пресненский район |
| 3 | Глубокий переулок | 17 | ЦАО | Пресненский район |
| 4 | Большой Гнездниковский переулок | 17 | ЦАО | Пресненский район |
# Оценим количество записей в таблице districts_df
len(districts_df)
4398
# Оценим, содержит ли столбец 'streetname' дубликаты и подумаем, с чем эти дубликаты могут быть связаны.
districts_df['streetname'].duplicated().sum()
794
Дубликатов в таблице districts_df 794 — это 18% записей в таблице. Много. Изучим строки с дубликатами.
districts_df[districts_df['streetname'].duplicated()].sort_values('streetname')
| streetname | areaid | okrug | area | |
|---|---|---|---|---|
| 4213 | 1-й Басманный переулок | 15 | ЦАО | Красносельский район |
| 4056 | 1-й Добрынинский переулок | 21 | ЦАО | Район Якиманка |
| 4328 | 1-й Коптельский переулок | 16 | ЦАО | Мещанский район |
| 3294 | 1-й Котляковский переулок | 101 | ЮАО | Район Москворечье-Сабурово |
| 2417 | 1-й Медведковский мост | 85 | СВАО | Район Южное Медведково |
| ... | ... | ... | ... | ... |
| 3783 | улица Юных Ленинцев | 121 | ЮВАО | Район Текстильщики |
| 890 | шоссе Энтузиастов | 32 | ВАО | Район Перово |
| 1023 | шоссе Энтузиастов | 36 | ВАО | Район Соколиная Гора |
| 1096 | шоссе Энтузиастов | 37 | ВАО | Район Ивановское |
| 3554 | шоссе Энтузиастов | 114 | ЮВАО | Район Лефортово |
794 rows × 4 columns
Мы видим, что дубликаты связаны с тем, что одна улица может находиться в разных округах и районах. Получается, почти 20% улиц в нашей таблице находятся в разных районах. Это важная информация! Далее будем считать, что если улица находится в разных районах, то это прямым образом влияет на популярность каждого из районов при составлении рейтинга. Добавим таблице most_popular_streets ещё два столбца из таблицы districts_df с помощью метода merge()
# Сохраним в таблице regions_df только название улицы и округа
regions_df = districts_df[['streetname', 'okrug']]
regions_df.columns = ['street_name', 'region']
most_popular_streets = most_popular_streets.merge(regions_df, on='street_name', how='inner')
most_popular_streets.drop_duplicates(inplace=True)
most_popular_streets
| street_name | points | region | |
|---|---|---|---|
| 0 | проспект Мира | 204 | СВАО |
| 6 | проспект Мира | 204 | ЦАО |
| 7 | Профсоюзная улица | 179 | ЮЗАО |
| 13 | Ленинградский проспект | 170 | САО |
| 17 | Пресненская набережная | 167 | ЦАО |
| 18 | Варшавское шоссе | 161 | ЮАО |
| 24 | Варшавское шоссе | 161 | ЮЗАО |
| 26 | Ленинский проспект | 145 | ЗАО |
| 28 | Ленинский проспект | 145 | ЮАО |
| 29 | Ленинский проспект | 145 | ЮЗАО |
| 33 | Ленинский проспект | 145 | ЦАО |
| 34 | проспект Вернадского | 130 | ЗАО |
| 37 | проспект Вернадского | 130 | ЮЗАО |
| 39 | Кутузовский проспект | 112 | ЗАО |
| 41 | Каширское шоссе | 111 | ЮАО |
| 45 | Кировоградская улица | 106 | ЮАО |
# Создадим сводную таблицу по районам для наглядного графика.
most_popular_districts = most_popular_streets.pivot_table(index='region', values='street_name', aggfunc='count')
most_popular_districts.columns = ['points']
most_popular_districts['district_name'] = most_popular_districts.index
most_popular_districts.reset_index(drop=True, inplace=True)
most_popular_districts = most_popular_districts[['district_name', 'points']]
most_popular_districts.sort_values(by='points', ascending=False, inplace=True)
fig = px.bar(
most_popular_districts, x='district_name', y='points',
labels={'points': 'количество улиц', 'district_name': 'округ'},
title = 'Рейтинг округов г. Москвы по количеству улиц в ТОП-10 по числу заведений общественного питания',
color_discrete_sequence=['MediumSlateBlue']
)
fig.show()
Итак, самыми популярными районами, в которых расположены улицы, насчитывающие наибольшее число заведений общественного питания, являются ЮАО и ЮЗАО. Будем считать два этих округа наиболее привлекательными для открытия собственного кафе. На графике отсутствуют СЗАО, ВАО и ЮВАО — вероятно, причина в том, что эти районы не так привлекательны. Ещё можно обратить внимание на то, что почти все попавшие в топ улицы объединяет то, что они являются крупными транспортными магистралями: по ним ежедневно перемещается достаточно большое количество людей и эти магистрали чаще всего проходят через ЦАО. Изучим, в каких районах находятся улицы с наименьшим числом заведений общественного питания, чтобы развить это предположение и не рассматривать наименее привлекательные районы для нашего кафе.
# Сохраним в таблице least_popular_streets названия улиц, на которых расположено всего одно заведение общественного питания
least_popular_streets = popular_streets.query('points == 1')
# Изучим количество записей
len(least_popular_streets)
650
В Москве 650 улиц, на которых расположено всего одно кафе. Попробуем составить репрезентативную выборку: возьмём 30 записей из этих данных и для каждой улицы определим район. Это поможет нам явно определить округ-аутсайдер по привлекательности для открытия кафе.
least_popular_streets = least_popular_streets.head(30)
# Ознакомимся с данными
least_popular_streets
| street_name | points | |
|---|---|---|
| 1910 | улица Ремизова | 1 |
| 366 | Большой Предтеченский переулок | 1 |
| 135 | 2-я Сокольническая улица | 1 |
| 1995 | улица Шумилова | 1 |
| 1680 | улица Гончарова | 1 |
| 365 | Большой Полуярославский переулок | 1 |
| 1661 | улица Гамалеи | 1 |
| 1909 | улица Ращупкина | 1 |
| 1670 | улица Генерала Ермолова | 1 |
| 92 | 2-й Кадашёвский переулок | 1 |
| 130 | 2-я Прядильная улица | 1 |
| 372 | Большой Строченовский переулок | 1 |
| 1925 | улица Савельева | 1 |
| 64 | 1-я Пугачёвская улица | 1 |
| 377 | Большой Трёхсвятительский переулок | 1 |
| 1918 | улица Рокотова | 1 |
| 1919 | улица Рословка | 1 |
| 369 | Большой Симоновский переулок | 1 |
| 382 | Боровая улица | 1 |
| 1915 | улица Рогожский Посёлок | 1 |
| 391 | Брошевский переулок | 1 |
| 137 | 2-я Филёвская улица | 1 |
| 1658 | улица Высоцкого | 1 |
| 1913 | улица Рогова | 1 |
| 383 | Боровский проезд | 1 |
| 395 | Будайский проезд | 1 |
| 390 | Бродников переулок | 1 |
| 386 | Ботанический переулок | 1 |
| 1996 | улица Шумкина | 1 |
| 91 | 2-й Кабельный проезд | 1 |
least_popular_streets = least_popular_streets.merge(regions_df, on='street_name', how='inner')
least_popular_streets.drop_duplicates(inplace=True)
# Убедимся, что столбец был успешно добавлен
least_popular_streets
| street_name | points | region | |
|---|---|---|---|
| 0 | улица Ремизова | 1 | ЮЗАО |
| 1 | Большой Предтеченский переулок | 1 | ЦАО |
| 2 | 2-я Сокольническая улица | 1 | ВАО |
| 3 | улица Шумилова | 1 | ЮВАО |
| 4 | улица Гончарова | 1 | СВАО |
| 5 | Большой Полуярославский переулок | 1 | ЦАО |
| 7 | улица Гамалеи | 1 | СЗАО |
| 8 | улица Ращупкина | 1 | ЗАО |
| 9 | улица Генерала Ермолова | 1 | ЗАО |
| 10 | 2-й Кадашёвский переулок | 1 | ЦАО |
| 11 | 2-я Прядильная улица | 1 | ВАО |
| 12 | Большой Строченовский переулок | 1 | ЦАО |
| 13 | улица Савельева | 1 | ЦАО |
| 14 | 1-я Пугачёвская улица | 1 | ВАО |
| 15 | Большой Трёхсвятительский переулок | 1 | ЦАО |
| 16 | улица Рокотова | 1 | ЮЗАО |
| 17 | улица Рословка | 1 | СЗАО |
| 18 | Большой Симоновский переулок | 1 | ЦАО |
| 19 | Боровая улица | 1 | ЮВАО |
| 20 | улица Рогожский Посёлок | 1 | ЮВАО |
| 21 | Брошевский переулок | 1 | ЦАО |
| 22 | 2-я Филёвская улица | 1 | ЗАО |
| 23 | улица Рогова | 1 | СЗАО |
| 24 | Боровский проезд | 1 | ЗАО |
| 25 | Будайский проезд | 1 | СВАО |
| 26 | Бродников переулок | 1 | ЦАО |
| 27 | Ботанический переулок | 1 | ЦАО |
| 28 | улица Шумкина | 1 | ВАО |
| 29 | 2-й Кабельный проезд | 1 | ЮВАО |
# Определим на основе нашей выборки, какой из районов имеет больше всего улиц, на которых расположено всего одно кафе
least_popular_districts = least_popular_streets.pivot_table(index='region', values='street_name', aggfunc='count')
least_popular_districts.columns = ['points']
least_popular_districts['district_name'] = least_popular_districts.index
least_popular_districts.reset_index(drop=True, inplace=True)
least_popular_districts = least_popular_districts[['district_name', 'points']]
least_popular_districts.sort_values(by='points', ascending=False, inplace=True)
fig = px.bar(
least_popular_districts, x='district_name', y='points',
labels={'points': 'количество улиц', 'district_name': 'округ'},
title = 'Рейтинг округов г. Москвы по количеству улиц c единственным заведением общественного питания',
color_discrete_sequence=['MediumSlateBlue']
)
fig.show()
И со значительным отрывом самым популярным районом, в котором больше всего улиц в одним заведением общественного питания становится ЦАО. Будем считать его, а также ВАО, ЗАО и ЮВАО (они делят второе место) наименее привлекательными районами для открытия кафе. Причина так же может быть связана с тем, что в ЦАО много маленьких улиц и они близко расположены друг к другу, для того, чтобы на них оказывалось большее число заведений общественного питания. ЮЗАО и ЮАО, лидеров прошлого графика, в этом графике не оказалось — это подтверждает наше предположение о том, что южный и юго-западный округа являются привлекательными.
Теперь обратим внимание на распределение количества посадочных мест для улиц с большим количеством объектов общественного питания и постараемся выявить закономерности.
# Сохраним в списке popular_streets значения с названиями улиц из таблицы с ТОП-10 улиц по популярности
popular_streets = list(most_popular_streets['street_name'])
# Сохраним в таблице popular_df только те строки из таблицы df, у которых названия улиц соответствуют ТОП-10
popular_df = df.query('street_name in @popular_streets')
# Убедимся, что таблица сохранена успешно
popular_df.head()
| id | object_name | chain | object_type | address | number | capacity_type | street_name | |
|---|---|---|---|---|---|---|---|---|
| 151 | 155973 | Кафе «Андерсон» | True | кафе | город Москва, Варшавское шоссе, дом 2 | 150 | много мест | Варшавское шоссе |
| 152 | 23618 | Subway | True | кафе | город Москва, Варшавское шоссе, дом 7, корпус 1 | 36 | мало мест | Варшавское шоссе |
| 153 | 155852 | Кафе «Ламаджо» | False | кафе | город Москва, Варшавское шоссе, дом 29 | 30 | мало мест | Варшавское шоссе |
| 154 | 152556 | Шаурма в пите | False | предприятие быстрого обслуживания | город Москва, Варшавское шоссе, дом 72, корпус 2 | 0 | мало мест | Варшавское шоссе |
| 155 | 120658 | Выпечка | False | кафетерий | город Москва, Варшавское шоссе, дом 100 | 2 | мало мест | Варшавское шоссе |
# Изучим данные и выбросы в них
popular_df['number'].describe()
count 1485.000000 mean 58.337374 std 90.421341 min 0.000000 25% 12.000000 50% 40.000000 75% 76.000000 max 1700.000000 Name: number, dtype: float64
# Избавимся от выбросов, оставив в столбце number значения от 1 и до 150 мест (это чуть больше, чем + 1,5 межквартильных
# размаха к медиане)
popular_df = popular_df.query('0 < number < 150')
plt.figure(figsize=(30,10))
sns.distplot(popular_df['number'], bins=30)
plt.xlabel('количество мест')
plt.ylabel('частота')
plt.title('График распределения количества мест в заведениях общественного питания на 10 самых популярных улицах')
plt.show()
Видим пик значений распределения в районе числа 20, а также следующее по частоте значение в районе числа 40 с последующим плавным затуханием при увеличении числа мест. Посмотрим, как себя чувствуют медианные значения количества мест для каждой из улиц в ТОП-10. Если мы решим открыть кафе на той или иной улице, то следует учитывать типичные для этой улицы значения.
avg_seats = popular_df.pivot_table(index='street_name', values='number', aggfunc='median')
avg_seats.columns = ['seats']
avg_seats['street_name'] = avg_seats.index
avg_seats.reset_index(drop=True, inplace=True)
avg_seats = avg_seats[['street_name', 'seats']]
avg_seats['seats'] = avg_seats['seats'].astype('int')
avg_seats.sort_values(by='seats', ascending=False, inplace=True)
fig = px.bar(
avg_seats, x='street_name', y='seats',
labels={'seats': 'количество мест', 'street_name': 'улица'},
title = 'Медианные значения количества мест в заведениях общественного питания на улицах из ТОП-10',
color_discrete_sequence=['LightSkyBlue']
)
fig.show()
Основываясь на графике, можно сделать вывод, что заведения общественного питания, которые расположены на проспектах, чаще всего имеют больше посадочных мест, а те, которые стоят возле шоссе — в основном менее вместительны. Учтём и эту особенность.
В результате проведённого исследования мы выяснили, что наиболее распространённым типом заведения общественного питания является кафе, при этом количество несетевых заведений значимо больше, чем сетевых. Самыми многочисленными среди сетевых заведений общественного питания являются заведения типа "предприятие быстрого обслуживания". Для сетевых заведений характерно большое число точек с небольшим числом посадочных мест, что логично, ведь если иметь в виду, что самый популярный тип заведений — "предприятие быстрого обслуживания", то совсем не нужно много мест, когда обслуживание происходит быстро, а ещё фастфуд имеет массу поклонников, поэтому таких заведений действительно много. В среднем самое большое число посадочных мест можно наблюдать в столовых: цены на комплексные обеды привлекают большое количество посетителей и, возможно, по этой причине, столовые практически никогда не оказываются сетевыми, каждая столовая способна к автономному существованию.
Наибольшее число заведений общественного питания находится на улицах ЮВАО и ЮЗАО, а больше всего улиц, имеющих по одному кафе — в центре и в Юго-Восточном округе. Если ситуация с ЦАО объяснима большим количеством маленьких улиц близко расположенных друг к другу, то ЮВАО — явно непривлекательный округ для открытия кафе. В среднем заведения общественного питания имеют около 40 посадочных мест. Эта цифра различна в зависимости от того, где находится заведение: на проспектах значение числа посадочных мест будет выше, а, если заведение расположено у шоссе, то, скорее всего, оно будет обладать меньшим числом мест.
Для открытия кафе можно остановить выбор на Профсоюзной улице — это улица с наибольшим числом заведений общественного питания в одном из двух самых популярных административных округов города Москвы. По количеству заведений её опережает только Проспект Мира, но он находится в СВАО и ЦАО.
Презентация: https://disk.yandex.ru/i/shJWEhGXTaSkKg